/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.cf.direct;

import java.io.IOException;

import com.android.dx.cf.attrib.AttAnnotationDefault;
import com.android.dx.cf.attrib.AttCode;
import com.android.dx.cf.attrib.AttConstantValue;
import com.android.dx.cf.attrib.AttDeprecated;
import com.android.dx.cf.attrib.AttEnclosingMethod;
import com.android.dx.cf.attrib.AttExceptions;
import com.android.dx.cf.attrib.AttInnerClasses;
import com.android.dx.cf.attrib.AttLineNumberTable;
import com.android.dx.cf.attrib.AttLocalVariableTable;
import com.android.dx.cf.attrib.AttLocalVariableTypeTable;
import com.android.dx.cf.attrib.AttRuntimeInvisibleAnnotations;
import com.android.dx.cf.attrib.AttRuntimeInvisibleParameterAnnotations;
import com.android.dx.cf.attrib.AttRuntimeVisibleAnnotations;
import com.android.dx.cf.attrib.AttRuntimeVisibleParameterAnnotations;
import com.android.dx.cf.attrib.AttSignature;
import com.android.dx.cf.attrib.AttSourceFile;
import com.android.dx.cf.attrib.AttSynthetic;
import com.android.dx.cf.attrib.InnerClassList;
import com.android.dx.cf.code.ByteCatchList;
import com.android.dx.cf.code.BytecodeArray;
import com.android.dx.cf.code.LineNumberList;
import com.android.dx.cf.code.LocalVariableList;
import com.android.dx.cf.iface.Attribute;
import com.android.dx.cf.iface.ParseException;
import com.android.dx.cf.iface.ParseObserver;
import com.android.dx.cf.iface.StdAttributeList;
import com.android.dx.rop.annotation.AnnotationVisibility;
import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.AnnotationsList;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.cst.Constant;
import com.android.dx.rop.cst.ConstantPool;
import com.android.dx.rop.cst.CstNat;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.cst.TypedConstant;
import com.android.dx.rop.type.TypeList;
import com.android.dx.util.ByteArray;
import com.android.dx.util.Hex;

/**
 * Standard subclass of {@link AttributeFactory}, which knows how to parse all
 * the standard attribute types.
 */
public class StdAttributeFactory extends AttributeFactory {

	/** {@code non-null;} shared instance of this class */
	public static final StdAttributeFactory THE_ONE = new StdAttributeFactory();

	/**
	 * Constructs an instance.
	 */
	public StdAttributeFactory() {
		// This space intentionally left blank.
	}

	/** {@inheritDoc} */
	@Override
	protected Attribute parse0(DirectClassFile cf, int context, String name,
			int offset, int length, ParseObserver observer) {
		switch (context) {
		case CTX_CLASS: {
			if (name == AttDeprecated.ATTRIBUTE_NAME) {
				return deprecated(cf, offset, length, observer);
			}
			if (name == AttEnclosingMethod.ATTRIBUTE_NAME) {
				return enclosingMethod(cf, offset, length, observer);
			}
			if (name == AttInnerClasses.ATTRIBUTE_NAME) {
				return innerClasses(cf, offset, length, observer);
			}
			if (name == AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME) {
				return runtimeInvisibleAnnotations(cf, offset, length, observer);
			}
			if (name == AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME) {
				return runtimeVisibleAnnotations(cf, offset, length, observer);
			}
			if (name == AttSynthetic.ATTRIBUTE_NAME) {
				return synthetic(cf, offset, length, observer);
			}
			if (name == AttSignature.ATTRIBUTE_NAME) {
				return signature(cf, offset, length, observer);
			}
			if (name == AttSourceFile.ATTRIBUTE_NAME) {
				return sourceFile(cf, offset, length, observer);
			}
			break;
		}
		case CTX_FIELD: {
			if (name == AttConstantValue.ATTRIBUTE_NAME) {
				return constantValue(cf, offset, length, observer);
			}
			if (name == AttDeprecated.ATTRIBUTE_NAME) {
				return deprecated(cf, offset, length, observer);
			}
			if (name == AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME) {
				return runtimeInvisibleAnnotations(cf, offset, length, observer);
			}
			if (name == AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME) {
				return runtimeVisibleAnnotations(cf, offset, length, observer);
			}
			if (name == AttSignature.ATTRIBUTE_NAME) {
				return signature(cf, offset, length, observer);
			}
			if (name == AttSynthetic.ATTRIBUTE_NAME) {
				return synthetic(cf, offset, length, observer);
			}
			break;
		}
		case CTX_METHOD: {
			if (name == AttAnnotationDefault.ATTRIBUTE_NAME) {
				return annotationDefault(cf, offset, length, observer);
			}
			if (name == AttCode.ATTRIBUTE_NAME) {
				return code(cf, offset, length, observer);
			}
			if (name == AttDeprecated.ATTRIBUTE_NAME) {
				return deprecated(cf, offset, length, observer);
			}
			if (name == AttExceptions.ATTRIBUTE_NAME) {
				return exceptions(cf, offset, length, observer);
			}
			if (name == AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME) {
				return runtimeInvisibleAnnotations(cf, offset, length, observer);
			}
			if (name == AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME) {
				return runtimeVisibleAnnotations(cf, offset, length, observer);
			}
			if (name == AttRuntimeInvisibleParameterAnnotations.ATTRIBUTE_NAME) {
				return runtimeInvisibleParameterAnnotations(cf, offset, length,
						observer);
			}
			if (name == AttRuntimeVisibleParameterAnnotations.ATTRIBUTE_NAME) {
				return runtimeVisibleParameterAnnotations(cf, offset, length,
						observer);
			}
			if (name == AttSignature.ATTRIBUTE_NAME) {
				return signature(cf, offset, length, observer);
			}
			if (name == AttSynthetic.ATTRIBUTE_NAME) {
				return synthetic(cf, offset, length, observer);
			}
			break;
		}
		case CTX_CODE: {
			if (name == AttLineNumberTable.ATTRIBUTE_NAME) {
				return lineNumberTable(cf, offset, length, observer);
			}
			if (name == AttLocalVariableTable.ATTRIBUTE_NAME) {
				return localVariableTable(cf, offset, length, observer);
			}
			if (name == AttLocalVariableTypeTable.ATTRIBUTE_NAME) {
				return localVariableTypeTable(cf, offset, length, observer);
			}
			break;
		}
		}

		return super.parse0(cf, context, name, offset, length, observer);
	}

	/**
	 * Parses an {@code AnnotationDefault} attribute.
	 */
	private Attribute annotationDefault(DirectClassFile cf, int offset,
			int length, ParseObserver observer) {
		if (length < 2) {
			throwSeverelyTruncated();
		}

		AnnotationParser ap = new AnnotationParser(cf, offset, length, observer);
		Constant cst = ap.parseValueAttribute();

		return new AttAnnotationDefault(cst, length);
	}

	/**
	 * Parses a {@code Code} attribute.
	 */
	private Attribute code(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length < 12) {
			return throwSeverelyTruncated();
		}

		ByteArray bytes = cf.getBytes();
		ConstantPool pool = cf.getConstantPool();
		int maxStack = bytes.getUnsignedShort(offset); // u2 max_stack
		int maxLocals = bytes.getUnsignedShort(offset + 2); // u2 max_locals
		int codeLength = bytes.getInt(offset + 4); // u4 code_length
		int origOffset = offset;

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "max_stack: " + Hex.u2(maxStack));
			observer.parsed(bytes, offset + 2, 2,
					"max_locals: " + Hex.u2(maxLocals));
			observer.parsed(bytes, offset + 4, 4,
					"code_length: " + Hex.u4(codeLength));
		}

		offset += 8;
		length -= 8;

		if (length < (codeLength + 4)) {
			return throwTruncated();
		}

		int codeOffset = offset;
		offset += codeLength;
		length -= codeLength;
		BytecodeArray code = new BytecodeArray(bytes.slice(codeOffset,
				codeOffset + codeLength), pool);
		if (observer != null) {
			code.forEach(new CodeObserver(code.getBytes(), observer));
		}

		// u2 exception_table_length
		int exceptionTableLength = bytes.getUnsignedShort(offset);
		ByteCatchList catches = (exceptionTableLength == 0) ? ByteCatchList.EMPTY
				: new ByteCatchList(exceptionTableLength);

		if (observer != null) {
			observer.parsed(bytes, offset, 2,
					"exception_table_length: " + Hex.u2(exceptionTableLength));
		}

		offset += 2;
		length -= 2;

		if (length < (exceptionTableLength * 8 + 2)) {
			return throwTruncated();
		}

		for (int i = 0; i < exceptionTableLength; i++) {
			if (observer != null) {
				observer.changeIndent(1);
			}

			int startPc = bytes.getUnsignedShort(offset);
			int endPc = bytes.getUnsignedShort(offset + 2);
			int handlerPc = bytes.getUnsignedShort(offset + 4);
			int catchTypeIdx = bytes.getUnsignedShort(offset + 6);
			CstType catchType = (CstType) pool.get0Ok(catchTypeIdx);
			catches.set(i, startPc, endPc, handlerPc, catchType);
			if (observer != null) {
				observer.parsed(
						bytes,
						offset,
						8,
						Hex.u2(startPc)
								+ ".."
								+ Hex.u2(endPc)
								+ " -> "
								+ Hex.u2(handlerPc)
								+ " "
								+ ((catchType == null) ? "<any>" : catchType
										.toHuman()));
			}
			offset += 8;
			length -= 8;

			if (observer != null) {
				observer.changeIndent(-1);
			}
		}

		catches.setImmutable();

		AttributeListParser parser = new AttributeListParser(cf, CTX_CODE,
				offset, this);
		parser.setObserver(observer);

		StdAttributeList attributes = parser.getList();
		attributes.setImmutable();

		int attributeByteCount = parser.getEndOffset() - offset;
		if (attributeByteCount != length) {
			return throwBadLength(attributeByteCount + (offset - origOffset));
		}

		return new AttCode(maxStack, maxLocals, code, catches, attributes);
	}

	/**
	 * Parses a {@code ConstantValue} attribute.
	 */
	private Attribute constantValue(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length != 2) {
			return throwBadLength(2);
		}

		ByteArray bytes = cf.getBytes();
		ConstantPool pool = cf.getConstantPool();
		int idx = bytes.getUnsignedShort(offset);
		TypedConstant cst = (TypedConstant) pool.get(idx);
		Attribute result = new AttConstantValue(cst);

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "value: " + cst);
		}

		return result;
	}

	/**
	 * Parses a {@code Deprecated} attribute.
	 */
	private Attribute deprecated(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length != 0) {
			return throwBadLength(0);
		}

		return new AttDeprecated();
	}

	/**
	 * Parses an {@code EnclosingMethod} attribute.
	 */
	private Attribute enclosingMethod(DirectClassFile cf, int offset,
			int length, ParseObserver observer) {
		if (length != 4) {
			throwBadLength(4);
		}

		ByteArray bytes = cf.getBytes();
		ConstantPool pool = cf.getConstantPool();

		int idx = bytes.getUnsignedShort(offset);
		CstType type = (CstType) pool.get(idx);

		idx = bytes.getUnsignedShort(offset + 2);
		CstNat method = (CstNat) pool.get0Ok(idx);

		Attribute result = new AttEnclosingMethod(type, method);

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "class: " + type);
			observer.parsed(bytes, offset + 2, 2,
					"method: " + DirectClassFile.stringOrNone(method));
		}

		return result;
	}

	/**
	 * Parses an {@code Exceptions} attribute.
	 */
	private Attribute exceptions(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length < 2) {
			return throwSeverelyTruncated();
		}

		ByteArray bytes = cf.getBytes();
		int count = bytes.getUnsignedShort(offset); // number_of_exceptions

		if (observer != null) {
			observer.parsed(bytes, offset, 2,
					"number_of_exceptions: " + Hex.u2(count));
		}

		offset += 2;
		length -= 2;

		if (length != (count * 2)) {
			throwBadLength((count * 2) + 2);
		}

		TypeList list = cf.makeTypeList(offset, count);
		return new AttExceptions(list);
	}

	/**
	 * Parses an {@code InnerClasses} attribute.
	 */
	private Attribute innerClasses(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length < 2) {
			return throwSeverelyTruncated();
		}

		ByteArray bytes = cf.getBytes();
		ConstantPool pool = cf.getConstantPool();
		int count = bytes.getUnsignedShort(offset); // number_of_classes

		if (observer != null) {
			observer.parsed(bytes, offset, 2,
					"number_of_classes: " + Hex.u2(count));
		}

		offset += 2;
		length -= 2;

		if (length != (count * 8)) {
			throwBadLength((count * 8) + 2);
		}

		InnerClassList list = new InnerClassList(count);

		for (int i = 0; i < count; i++) {
			int innerClassIdx = bytes.getUnsignedShort(offset);
			int outerClassIdx = bytes.getUnsignedShort(offset + 2);
			int nameIdx = bytes.getUnsignedShort(offset + 4);
			int accessFlags = bytes.getUnsignedShort(offset + 6);
			CstType innerClass = (CstType) pool.get(innerClassIdx);
			CstType outerClass = (CstType) pool.get0Ok(outerClassIdx);
			CstString name = (CstString) pool.get0Ok(nameIdx);
			list.set(i, innerClass, outerClass, name, accessFlags);
			if (observer != null) {
				observer.parsed(bytes, offset, 2, "inner_class: "
						+ DirectClassFile.stringOrNone(innerClass));
				observer.parsed(bytes, offset + 2, 2, "  outer_class: "
						+ DirectClassFile.stringOrNone(outerClass));
				observer.parsed(bytes, offset + 4, 2, "  name: "
						+ DirectClassFile.stringOrNone(name));
				observer.parsed(bytes, offset + 6, 2, "  access_flags: "
						+ AccessFlags.innerClassString(accessFlags));
			}
			offset += 8;
		}

		list.setImmutable();
		return new AttInnerClasses(list);
	}

	/**
	 * Parses a {@code LineNumberTable} attribute.
	 */
	private Attribute lineNumberTable(DirectClassFile cf, int offset,
			int length, ParseObserver observer) {
		if (length < 2) {
			return throwSeverelyTruncated();
		}

		ByteArray bytes = cf.getBytes();
		int count = bytes.getUnsignedShort(offset); // line_number_table_length

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "line_number_table_length: "
					+ Hex.u2(count));
		}

		offset += 2;
		length -= 2;

		if (length != (count * 4)) {
			throwBadLength((count * 4) + 2);
		}

		LineNumberList list = new LineNumberList(count);

		for (int i = 0; i < count; i++) {
			int startPc = bytes.getUnsignedShort(offset);
			int lineNumber = bytes.getUnsignedShort(offset + 2);
			list.set(i, startPc, lineNumber);
			if (observer != null) {
				observer.parsed(bytes, offset, 4, Hex.u2(startPc) + " "
						+ lineNumber);
			}
			offset += 4;
		}

		list.setImmutable();
		return new AttLineNumberTable(list);
	}

	/**
	 * Parses a {@code LocalVariableTable} attribute.
	 */
	private Attribute localVariableTable(DirectClassFile cf, int offset,
			int length, ParseObserver observer) {
		if (length < 2) {
			return throwSeverelyTruncated();
		}

		ByteArray bytes = cf.getBytes();
		int count = bytes.getUnsignedShort(offset);

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "local_variable_table_length: "
					+ Hex.u2(count));
		}

		LocalVariableList list = parseLocalVariables(
				bytes.slice(offset + 2, offset + length), cf.getConstantPool(),
				observer, count, false);
		return new AttLocalVariableTable(list);
	}

	/**
	 * Parses a {@code LocalVariableTypeTable} attribute.
	 */
	private Attribute localVariableTypeTable(DirectClassFile cf, int offset,
			int length, ParseObserver observer) {
		if (length < 2) {
			return throwSeverelyTruncated();
		}

		ByteArray bytes = cf.getBytes();
		int count = bytes.getUnsignedShort(offset);

		if (observer != null) {
			observer.parsed(bytes, offset, 2,
					"local_variable_type_table_length: " + Hex.u2(count));
		}

		LocalVariableList list = parseLocalVariables(
				bytes.slice(offset + 2, offset + length), cf.getConstantPool(),
				observer, count, true);
		return new AttLocalVariableTypeTable(list);
	}

	/**
	 * Parse the table part of either a {@code LocalVariableTable} or a
	 * {@code LocalVariableTypeTable}.
	 * 
	 * @param bytes
	 *            {@code non-null;} bytes to parse, which should <i>only</i>
	 *            contain the table data (no header)
	 * @param pool
	 *            {@code non-null;} constant pool to use
	 * @param count
	 *            {@code >= 0;} the number of entries
	 * @param typeTable
	 *            {@code true} iff this is for a type table
	 * @return {@code non-null;} the constructed list
	 */
	private LocalVariableList parseLocalVariables(ByteArray bytes,
			ConstantPool pool, ParseObserver observer, int count,
			boolean typeTable) {
		if (bytes.size() != (count * 10)) {
			// "+ 2" is for the count.
			throwBadLength((count * 10) + 2);
		}

		ByteArray.MyDataInputStream in = bytes.makeDataInputStream();
		LocalVariableList list = new LocalVariableList(count);

		try {
			for (int i = 0; i < count; i++) {
				int startPc = in.readUnsignedShort();
				int length = in.readUnsignedShort();
				int nameIdx = in.readUnsignedShort();
				int typeIdx = in.readUnsignedShort();
				int index = in.readUnsignedShort();
				CstString name = (CstString) pool.get(nameIdx);
				CstString type = (CstString) pool.get(typeIdx);
				CstString descriptor = null;
				CstString signature = null;

				if (typeTable) {
					signature = type;
				} else {
					descriptor = type;
				}

				list.set(i, startPc, length, name, descriptor, signature, index);

				if (observer != null) {
					observer.parsed(bytes, i * 10, 10, Hex.u2(startPc) + ".."
							+ Hex.u2(startPc + length) + " " + Hex.u2(index)
							+ " " + name.toHuman() + " " + type.toHuman());
				}
			}
		} catch (IOException ex) {
			throw new RuntimeException("shouldn't happen", ex);
		}

		list.setImmutable();
		return list;
	}

	/**
	 * Parses a {@code RuntimeInvisibleAnnotations} attribute.
	 */
	private Attribute runtimeInvisibleAnnotations(DirectClassFile cf,
			int offset, int length, ParseObserver observer) {
		if (length < 2) {
			throwSeverelyTruncated();
		}

		AnnotationParser ap = new AnnotationParser(cf, offset, length, observer);
		Annotations annotations = ap
				.parseAnnotationAttribute(AnnotationVisibility.BUILD);

		return new AttRuntimeInvisibleAnnotations(annotations, length);
	}

	/**
	 * Parses a {@code RuntimeVisibleAnnotations} attribute.
	 */
	private Attribute runtimeVisibleAnnotations(DirectClassFile cf, int offset,
			int length, ParseObserver observer) {
		if (length < 2) {
			throwSeverelyTruncated();
		}

		AnnotationParser ap = new AnnotationParser(cf, offset, length, observer);
		Annotations annotations = ap
				.parseAnnotationAttribute(AnnotationVisibility.RUNTIME);

		return new AttRuntimeVisibleAnnotations(annotations, length);
	}

	/**
	 * Parses a {@code RuntimeInvisibleParameterAnnotations} attribute.
	 */
	private Attribute runtimeInvisibleParameterAnnotations(DirectClassFile cf,
			int offset, int length, ParseObserver observer) {
		if (length < 2) {
			throwSeverelyTruncated();
		}

		AnnotationParser ap = new AnnotationParser(cf, offset, length, observer);
		AnnotationsList list = ap
				.parseParameterAttribute(AnnotationVisibility.BUILD);

		return new AttRuntimeInvisibleParameterAnnotations(list, length);
	}

	/**
	 * Parses a {@code RuntimeVisibleParameterAnnotations} attribute.
	 */
	private Attribute runtimeVisibleParameterAnnotations(DirectClassFile cf,
			int offset, int length, ParseObserver observer) {
		if (length < 2) {
			throwSeverelyTruncated();
		}

		AnnotationParser ap = new AnnotationParser(cf, offset, length, observer);
		AnnotationsList list = ap
				.parseParameterAttribute(AnnotationVisibility.RUNTIME);

		return new AttRuntimeVisibleParameterAnnotations(list, length);
	}

	/**
	 * Parses a {@code Signature} attribute.
	 */
	private Attribute signature(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length != 2) {
			throwBadLength(2);
		}

		ByteArray bytes = cf.getBytes();
		ConstantPool pool = cf.getConstantPool();
		int idx = bytes.getUnsignedShort(offset);
		CstString cst = (CstString) pool.get(idx);
		Attribute result = new AttSignature(cst);

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "signature: " + cst);
		}

		return result;
	}

	/**
	 * Parses a {@code SourceFile} attribute.
	 */
	private Attribute sourceFile(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length != 2) {
			throwBadLength(2);
		}

		ByteArray bytes = cf.getBytes();
		ConstantPool pool = cf.getConstantPool();
		int idx = bytes.getUnsignedShort(offset);
		CstString cst = (CstString) pool.get(idx);
		Attribute result = new AttSourceFile(cst);

		if (observer != null) {
			observer.parsed(bytes, offset, 2, "source: " + cst);
		}

		return result;
	}

	/**
	 * Parses a {@code Synthetic} attribute.
	 */
	private Attribute synthetic(DirectClassFile cf, int offset, int length,
			ParseObserver observer) {
		if (length != 0) {
			return throwBadLength(0);
		}

		return new AttSynthetic();
	}

	/**
	 * Throws the right exception when a known attribute has a way too short
	 * length.
	 * 
	 * @return never
	 * @throws ParseException
	 *             always thrown
	 */
	private static Attribute throwSeverelyTruncated() {
		throw new ParseException("severely truncated attribute");
	}

	/**
	 * Throws the right exception when a known attribute has a too short length.
	 * 
	 * @return never
	 * @throws ParseException
	 *             always thrown
	 */
	private static Attribute throwTruncated() {
		throw new ParseException("truncated attribute");
	}

	/**
	 * Throws the right exception when an attribute has an unexpected length
	 * (given its contents).
	 * 
	 * @param expected
	 *            expected length
	 * @return never
	 * @throws ParseException
	 *             always thrown
	 */
	private static Attribute throwBadLength(int expected) {
		throw new ParseException("bad attribute length; expected length "
				+ Hex.u4(expected));
	}
}
